/*
 * Copyright (C) 2010-2011 Comcast Cable.  All Rights Reserved.
 */
package com.hercules.android.receiver.handlers.events.listener;

import android.graphics.PointF;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;

import com.hercules.android.receiver.AndroidReceiver;
import com.hercules.android.receiver.LogUtil;
import com.hercules.android.receiver.handlers.events.EventRaiser;
import com.hercules.android.receiver.handlers.events.EventRaisingException;
import com.hercules.android.receiver.handlers.events.EventsUtil;
import com.hercules.android.receiver.model.events.XREEventPhase;
import com.hercules.android.receiver.model.events.XREMouseButton;
import com.hercules.android.receiver.model.events.XREMouseEvent;
import com.hercules.android.receiver.model.objects.XREObject;
import com.hercules.android.receiver.model.objects.XREView;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.logging.Logger;

/**
 * <p>
 * This is the listener that converts an Android touch event to a XREMouseEvent, then raises the preview and actual
 * events respectively. It only reacts to the event whose type is given by super.eventType
 * </p>
 * <p>
 * <strong>Thread safety: </strong> This class is thread-safe because the base class is thread-safe and the class is
 * immutable.
 * </p>
 *
 * @author mekanizumu, jaindsonvs
 * @author 7realm
 * @version 1.0
 */
public class MouseEventListener extends BaseListener implements OnTouchListener {
    /** Represents how much can vertical line differ from actual line. */
    private static final int VERTICAL_PRESICION = 5;
    /** Represents how far can points be. */
    private static final int MIN_DISTANCE = 5;
    /** Represents adjust multiplier for wheel data. */
    private static final double WHEEL_DATA_MULTIPLIER = 0.2;
    /**
     * <p>
     * The logger used for logging.
     * </p>
     * <p>
     * Initialization and Mutability: It is final and won't change once it is initialized as part of variable
     * declaration to: Logger.getLogger(MouseEventListener.class.getName()).
     * </p>
     * <p>
     * Usage: It's is used in all public methods.
     * </p>
     */
    private static final Logger LOGGER = Logger.getLogger(MouseEventListener.class.getSimpleName());
    /**
     * Represents points where mouse was moved.
     * <p>
     * AndroidAssembly3 added to support onMouseWheel event.
     */
    private final List<PointF> points = Collections.synchronizedList(new ArrayList<PointF>());

    /**
     * <p>
     * Create an instance of the class.
     * </p>
     *
     * @param eventRaiser the EventRaiser used to raise a XRE event.
     * @param source the source of the event.
     * @param receiver the receiver that will be passed to Android listeners of this component.
     * @param eventType the event type that the listener will react to.
     * @throws IllegalArgumentException if receiver doesn't conform to the following legal value specification: It
     * cannot be null. Or source doesn't conform to the following legal value specification: It cannot be null. Or
     * eventType doesn't conform to the following legal value specification: It cannot be null or empty. Or eventRaiser
     * doesn't conform to the following legal value specification: It cannot be null.
     */
    public MouseEventListener(AndroidReceiver receiver, XREObject source, String eventType, EventRaiser eventRaiser) {
        super(receiver, source, eventType, eventRaiser);
    }

    /**
     * <p>
     * This method is called when a touch event is dispatched to a view.
     * </p>
     * <p>
     * AndroidAssembly1 refactoring and logging, also true always is returned
     * <p>
     * AndroidAssembly2 changed to support multiple event handlers, getEventType is ignored
     * <p>
     * AndroidAssembly3 changed to support onMouseWheel event
     *
     * @param v the view the touch event has been dispatched to.
     * @param event the MotionEvent object containing full information about the event.
     * @throws IllegalArgumentException if v is null; Or event is null;
     * @return always true
     */
    @SuppressWarnings("boxing")
    public boolean onTouch(View v, MotionEvent event) {
        final String methodName = "onTouch";
        LogUtil.enter(LOGGER, this, methodName);

        try {
            // check arguments
            EventsUtil.checkArgumentNull(v, "v");
            EventsUtil.checkArgumentNull(event, "event");
        } catch (IllegalArgumentException e) {
            throw LogUtil.error(LOGGER, this, methodName, e);
        }

        String eventName;
        switch (event.getAction()) {
        case MotionEvent.ACTION_UP:
            eventName = XREMouseEvent.ON_MOUSE_UP_EVENT_NAME;
            points.add(new PointF(event.getX(), event.getY()));
            break;
        case MotionEvent.ACTION_DOWN:
            eventName = XREMouseEvent.ON_MOUSE_DOWN_EVENT_NAME;
            points.clear();
            points.add(new PointF(event.getX(), event.getY()));
            break;
        default:
            points.add(new PointF(event.getX(), event.getY()));
            return true;
        }

        LogUtil.info(LOGGER, this, methodName, "OnTouch action {0}; event name {1}, source {2}", event.getAction(),
            eventName, getSource().getId());

        // try to raise event
        boolean isHandled = false;
        try {
            // raise normal event
            isHandled = raiseEvent(event, eventName, 0);

            // check if onMouseWheel event must be raised
            if (event.getAction() == MotionEvent.ACTION_UP && checkTwoPoints()) {
                // check if drawn path is vertical
                float sx = 0;
                float sy = 0;
                float sx2 = 0;
                float sxy = 0;
                for (PointF point : points) {
                    sx += point.x;
                    sy += point.y;
                    sx2 += point.x * point.x;
                    sxy += point.x * point.y;
                }

                int n = points.size();
                if (n > 1) {
	                double k = (n * sxy - sx * sy) / (n * sx2 - sx * sx);
	                if (Math.abs(k) > VERTICAL_PRESICION || Math.abs(n * sx2 - sx * sx) < 0.000001) {
	                    float wheelData = points.get(points.size() - 1).y - points.get(0).y;
	                    wheelData *= WHEEL_DATA_MULTIPLIER;
	                    isHandled =
	                        isHandled || raiseEvent(event, XREMouseEvent.ON_MOUSE_WHEEL_EVENT_NAME, (int) wheelData);
	                }
                }
            }

            // clear points
            points.clear();
        } catch (EventRaisingException e) {
            LogUtil.error(LOGGER, getClass(), methodName, e);
        }

        // support onMouseUp event
        isHandled = isHandled || event.getAction() == MotionEvent.ACTION_DOWN;

        return LogUtil.exit(LOGGER, getClass(), methodName, isHandled);
    }

    /**
     * Check the case where we have only two points, for wheel event we should have some minimal distance between them.
     *
     * @return false if there are two points and distance is too small, otherwise true
     */
    private boolean checkTwoPoints() {
        if (points.size() == 2) {
            float dx = points.get(0).x - points.get(1).x;
            float dy = points.get(0).y - points.get(1).y;
            float distance = dx * dx + dy * dy;
            if (distance < MIN_DISTANCE * MIN_DISTANCE) {
                return false;
            }
        }

        return true;
    }

    /**
     * <p>
     * Tries to raise a {@link XREMouseEvent} event.
     * </p>
     * <p>
     * AndroidAssembly1 refactor, added support for ignore mouse
     * <p>
     * AndroidAssembly2 added support for multiple event handlers, getEventType is not used
     *
     * @param event the MotionEvent object containing full information about the event.
     * @param eventName the name of generated event
     * @param wheelData the wheel ratio
     * @return true if event was raised successfully
     * @throws EventRaisingException if error occurred while raising event
     */
    private boolean raiseEvent(MotionEvent event, String eventName, int wheelData) throws EventRaisingException {
        // check if source is XREView and mouse events are ignored
        if (getSource() instanceof XREView) {
            XREView xreView = (XREView) getSource();

            if (xreView.isIgnoreMouse()) {
                return false;
            }
        }

        // prepare event
        XREMouseEvent xreEvent = new XREMouseEvent();
        if (XREMouseEvent.ON_MOUSE_WHEEL_EVENT_NAME.equals(eventName)) {
        	xreEvent.setButton(XREMouseButton.CENTER);
        } else {
        	xreEvent.setButton(XREMouseButton.LEFT);
        }
        
        xreEvent.setX((int) event.getX());
        xreEvent.setY((int) event.getY());
        xreEvent.setWheelDelta(wheelData);
        xreEvent.setSource(getSource().getId());

        // this block sets the event to be a PREVIEW event and raise it
        xreEvent.setPhase(XREEventPhase.PREVIEW);
        setNextEventIndex(xreEvent);
        xreEvent.setName(eventName.substring(0, 2) + "Preview" + eventName.substring(2));
        boolean isHandled = getEventRaiser().raiseEvent(xreEvent, getReceiver(), false);

        // AndroidAssembly3 raise STANDARD event only if PREVIEW is not handled
        if (!isHandled) {
            // this block sets the event as a STANDARD event and raise it
            xreEvent.setPhase(XREEventPhase.STANDARD);
            setNextEventIndex(xreEvent);
            xreEvent.setName(eventName);
            return getEventRaiser().raiseEvent(xreEvent, getReceiver(), false);
        }

        return isHandled;
    }
}
